{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    },
    "polyglot_notebook": {
     "kernelName": "csharp"
    }
   },
   "source": [
    "# HttpClient 使用准则\n",
    "System.Net.Http.HttpClient 类用于发送 HTTP 请求以及从 URI 所标识的资源接收 HTTP 响应。 HttpClient 实例是应用于该实例执行的所有请求的设置集合，每个实例使用自身的连接池，该池将其请求与其他请求隔离开来。 \n",
    "\n",
    "从 .NET Core 2.1 开始，SocketsHttpHandler 类提供实现，使行为在所有平台上保持一致。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 准备工作：先执行下面单元，以启动WebApi及设置全局对象、方法及其它"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    },
    "polyglot_notebook": {
     "kernelName": "csharp"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "配置文件根目录：c:\\Users\\ruyu\\Desktop\\HttpClientStudy\\Docs\\Publish\\HttpClientStudy.Core\n",
      "程序[c:\\Users\\ruyu\\Desktop\\HttpClientStudy\\Docs\\Publish\\HttpClientStudy.WebApp\\HttpClientStudy.WebApp.exe]已在新的命令行窗口执行。如果未出现新命令行窗口，可能是程序错误造成窗口闪现！\n"
     ]
    }
   ],
   "source": [
    "//全局设置\n",
    "#r \"./Publish/HttpClientStudy.Core/HttpClientStudy.Core.dll\"\n",
    "\n",
    "//全局命名空间引用\n",
    "global using System.Net;\n",
    "global using System.Net.Http;\n",
    "global using System.Diagnostics;\n",
    "global using System.Collections;\n",
    "global using System.Text;\n",
    "global using System.Linq;\n",
    "global using System.IO;\n",
    "global using System.Threading;\n",
    "global using System.Threading.Tasks;\n",
    "\n",
    "global using HttpClientStudy.Core;\n",
    "global using HttpClientStudy.Core.Utilities;\n",
    "\n",
    "//全局共享变量\n",
    "var global_api_config = HttpClientStudy.Config.WebApiConfigManager.GetWebApiConfig();\n",
    "var global_ips = Dns.GetHostAddresses(global_api_config.Host);\n",
    "var global_queryIp = global_ips.First().ToString();\n",
    "if(global_api_config.Host.Contains(\"localhost\"))\n",
    "{\n",
    "    global_queryIp = \"127.0.0.1\";\n",
    "}\n",
    "var global_default_page = $\"{global_api_config.PathBase}/Normal/GetAllAccounts\";\n",
    "//启动WebAPI程序\n",
    "var global_netstat_filter = $\"{global_queryIp}:{global_api_config.Port}\";\n",
    "var global_webapi_file = Path.GetFullPath(\"./Publish/HttpClientStudy.WebApp/HttpClientStudy.WebApp.exe\", Environment.CurrentDirectory); \n",
    "var message = AppUtility.RunWebApiExeFile(global_webapi_file);\n",
    "Console.WriteLine(message);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "统一使用示例"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    },
    "polyglot_notebook": {
     "kernelName": "csharp"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [],
   "source": [
    "{ //大括号: 1、作用域隔离 2、方便整体代码折叠\n",
    "    Console.WriteLine(global_api_config.BaseUrl);\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 启动WebApi"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "dotnet_interactive": {
     "language": "pwsh"
    },
    "polyglot_notebook": {
     "kernelName": "pwsh"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [],
   "source": [
    "#启动已发布的WebApi项目\n",
    "# 使用dotnet命令启动的程序，进程名均为 dotnet，不好关闭\n",
    "# Start-Process -FilePath dotnet -ArgumentList \".\\Publish\\HttpClientStudy.WebApp\\HttpClientStudy.WebApp.dll\"\n",
    "\n",
    "# 此种，进程名固定\n",
    "Start-Process -FilePath \".\\Publish\\HttpClientStudy.WebApp\\HttpClientStudy.WebApp.exe\""
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 关闭WebApi"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "dotnet_interactive": {
     "language": "pwsh"
    },
    "polyglot_notebook": {
     "kernelName": "pwsh"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [],
   "source": [
    "# 关闭项目进程\n",
    "$WebAppProcName =\"HttpClientStudy.WebApp\";\n",
    "$WebAppProc = Get-Process $WebAppProcName -ErrorAction Ignore\n",
    "if($null -eq $WebAppProc)\n",
    "{\n",
    "    Write-Host \"进程没有找到，可能已经关闭\"\n",
    "}\n",
    "else {\n",
    "    $WebAppProc.Kill();\n",
    "    Write-Host \"$WebAppProcName 进程已退出\"\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    },
    "polyglot_notebook": {
     "kernelName": "csharp"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "source": [
    "## 1、DNS 行为\n",
    "HttpClient 仅在创建连接时解析 DNS。它不跟踪 DNS 服务器指定的任何生存时间 (TTL)。 \n",
    "\n",
    "如果 DNS 条目定期更改（这可能在某些方案中发生），客户端将不会遵循这些更新。 要解决此问题，可以通过设置 PooledConnectionLifetime 属性来限制连接的生存期，以便在替换连接时重复执行 DNS 查找。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    },
    "polyglot_notebook": {
     "kernelName": "csharp"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [],
   "source": [
    "using System.Net.Http;\n",
    "{\n",
    "    var handler = new SocketsHttpHandler\n",
    "    {\n",
    "        // 15分钟\n",
    "        PooledConnectionLifetime = TimeSpan.FromMinutes(15) \n",
    "    };\n",
    "    var sharedClient = new HttpClient(handler);\n",
    "\n",
    "    sharedClient.Display();\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "上述 HttpClient 配置为重复使用连接 15 分钟。 PooledConnectionLifetime 指定的时间范围过后，系统会关闭连接，然后创建一个新连接。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2、共用连接(底层自动管理连接池)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "HttpClient 的连接池链接到其基础 SocketsHttpHandler。 \n",
    "释放 HttpClient 实例时，它会释放池中的所有现有连接。 如果稍后向同一服务器发送请求，则必须重新创建一个新连接。 \n",
    "因此，创建不必要的连接会导致性能损失。 \n",
    "此外，TCP 端口不会在连接关闭后立即释放。 （有关这一点的详细信息，请参阅 RFC 9293 中的 TCP TIME-WAIT。）如果请求速率较高，则可用端口的操作系统限制可能会耗尽。 \n",
    "\n",
    "为了避免端口耗尽问题，建议将 HttpClient 实例重用于尽可能多的 HTTP 请求。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 什么是连接池\n",
    "SocketsHttpHandler为每个唯一端点建立连接池，您的应用程序通过HttpClient向该唯一端点发出出站HTTP请求。在对端点的第一个请求上，当不存在现有连接时，将建立一个新的HTTP连接并将其用于该请求。该请求完成后，连接将保持打开状态并返回到池中。\n",
    "\n",
    "对同一端点的后续请求将尝试从池中找到可用的连接。如果没有可用的连接，并且尚未达到该端点的连接限制，则将建立新的连接。达到连接限制后，请求将保留在队列中，直到连接可以自由发送它们为止。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 如何控制连接池"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "有三个主要设置可用于控制连接池的行为。\n",
    "\n",
    "+ PooledConnectionLifetime，定义连接在池中保持活动状态的时间。此生存期到期后，将不再为将来的请求而合并或发出连接。\n",
    "\n",
    "+ PooledConnectionIdleTimeout，定义闲置连接在未使用时在池中保留的时间。一旦此生存期到期，空闲连接将被清除并从池中删除。\n",
    "\n",
    "+ MaxConnectionsPerServer，定义每个端点将建立的最大出站连接数。每个端点的连接分别池化。例如，如果最大连接数为2，则您的应用程序将请求发送到两个www.github.com和www.google.com，总共可能最多有4个打开的连接。\n",
    "\n",
    "默认情况下，从.NET Core 2.1开始，更高级别的HttpClientHandler将SocketsHttpHandler用作内部处理程序。没有任何自定义配置，将应用连接池的默认设置。\n",
    "\n",
    "该**PooledConnectionLifetime默认是无限的，因此，虽然经常使用的请求，连接可能会无限期地保持打开状态。该PooledConnectionIdleTimeout默认为2分钟，如果在连接池中长时间未使用将被清理。MaxConnectionsPerServer**默认为int.MaxValue，因此连接基本上不受限制。\n",
    "\n",
    "如果希望控制这些值中的任何一个，则可以手动创建SocketsHttpHandler实例，并根据需要进行配置。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    },
    "polyglot_notebook": {
     "kernelName": "csharp"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [],
   "source": [
    "//手动配置 SocketsHttpHandler\n",
    "{\n",
    "\tvar socketsHandler = new SocketsHttpHandler\n",
    "\t{\n",
    "\t\tPooledConnectionLifetime = TimeSpan.FromMinutes(10),\n",
    "\t\tPooledConnectionIdleTimeout = TimeSpan.FromMinutes(5),\n",
    "\t\tMaxConnectionsPerServer = 10\n",
    "\t};\n",
    "\t\t\n",
    "\tvar client = new HttpClient(socketsHandler);\n",
    "\n",
    "\tclient.Display();\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在前面的示例中，对SocketsHttpHandler进行了配置，以使连接将最多在10分钟后停止重新发出并关闭。如果闲置5分钟，则连接将在池的清理过程中被更早地删除。我们还将最大连接数（每个端点）限制为十个。如果我们需要并行发出更多出站请求，则某些请求可能会排队等待，直到10个池中的连接可用为止。\n",
    "要应用处理程序，它将被传递到HttpClient的构造函数中。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 测试连接寿命"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    },
    "polyglot_notebook": {
     "kernelName": "csharp"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [],
   "source": [
    "//测试连接寿命\n",
    "{\n",
    "    Console.WriteLine(\"程序运行大约要10-20秒，请在程序退出后，执行下面命令行查看网络情况\");\n",
    "\n",
    "    //自定义行为\n",
    "    var socketsHandler = new SocketsHttpHandler\n",
    "    {\n",
    "        //连接池生命周期为10分钟：连接在池中保持活动时间为10分钟\n",
    "        PooledConnectionLifetime = TimeSpan.FromMinutes(10),\n",
    "\n",
    "        //池化链接的空闲超时时间为5分钟: 5分钟内连接不被重用，则被释放后销毁\n",
    "        PooledConnectionIdleTimeout = TimeSpan.FromMinutes(5),\n",
    "        \n",
    "        //每端点的最大连接数设置为10个\n",
    "        MaxConnectionsPerServer = 10\n",
    "    };\n",
    "\n",
    "    var client = new HttpClient(socketsHandler)\n",
    "    {\n",
    "        BaseAddress = new Uri(global_api_config.BaseUrl)\n",
    "    };\n",
    "\n",
    "    var displayer = \"\".Display();\n",
    "\n",
    "    for (var i = 0; i < 5; i++)\n",
    "    {\n",
    "        if(i>0)\n",
    "        {\n",
    "            await Task.Delay(TimeSpan.FromSeconds(2));\n",
    "        }\n",
    "        _ = await client.GetAsync(global_default_page);\n",
    "        displayer.Update(($\"第{i+1}次请求完成\"));\n",
    "        \n",
    "        await Task.Delay(TimeSpan.FromSeconds(2));\n",
    "    }\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "使用自定义设置，依次向同一端点发出5个请求。在每个请求之间，暂停两秒钟。输出从DNS检索到的网站服务器的IPv4地址。我们可以使用此IP地址来查看通过PowerShell中发出的netstat命令对其打开的连接："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "dotnet_interactive": {
     "language": "pwsh"
    },
    "polyglot_notebook": {
     "kernelName": "pwsh"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [],
   "source": [
    "# 若查询不到，则异常\n",
    "#!set --value @csharp:global_netstat_filter --name queryFilter\n",
    "\n",
    "Write-Host \"请先执行上面的单元，再执行本单元\"\n",
    "Write-Host \"网络状态\"\n",
    "netstat -ano | findstr $queryFilter"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在这种情况下，到远程端点的连接只有1个。在每个请求之后，该连接将返回到池中，因此在发出下一个请求时可以重新使用。\n",
    "如果更改连接的生存期，以使它们在1秒后过期，测试这对行为的影响："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    },
    "polyglot_notebook": {
     "kernelName": "csharp"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [],
   "source": [
    "//程序池设置\n",
    "{  \n",
    "    //自定义行为\n",
    "    Console.WriteLine(\"程序运行大约要10-20，请在程序退出后，执行下面命令行查看网络情况\");\n",
    "    var socketsHandler2 = new SocketsHttpHandler\n",
    "    {\n",
    "        PooledConnectionLifetime = TimeSpan.FromSeconds(1),\n",
    "        PooledConnectionIdleTimeout = TimeSpan.FromSeconds(1),\n",
    "        MaxConnectionsPerServer = 1\n",
    "    };\n",
    "\n",
    "    var client2 = new HttpClient(socketsHandler2)\n",
    "    {\n",
    "        BaseAddress = new Uri(global_api_config.BaseUrl)\n",
    "    };\n",
    "\n",
    "    var displayer = \"\".Display();\n",
    "\n",
    "    for (var i = 0; i < 5; i++)\n",
    "    {\n",
    "        if(i>0)\n",
    "        {\n",
    "            await Task.Delay(TimeSpan.FromSeconds(2));\n",
    "        }\n",
    "        _ = await client2.GetAsync(global_default_page);\n",
    "        displayer.Update(($\"第{i+1}次请求完成\"));\n",
    "        \n",
    "        await Task.Delay(TimeSpan.FromSeconds(2));\n",
    "    }\n",
    "   \n",
    "    //调用命令行，显示查看网络情况\n",
    "    string command = $\"netstat -ano | findstr {global_netstat_filter}\";\n",
    "    \n",
    "    // 创建一个新的ProcessStartInfo对象\n",
    "    ProcessStartInfo startInfo = new ProcessStartInfo(\"cmd\", $\"/c {command}\")\n",
    "    {\n",
    "        RedirectStandardOutput = true, // 重定向标准输出\n",
    "        UseShellExecute = false, // 不使用系统外壳程序启动\n",
    "        CreateNoWindow = true // 不创建新窗口\n",
    "    };\n",
    "    \n",
    "    // 启动进程\n",
    "    using (Process process = Process.Start(startInfo))\n",
    "    {\n",
    "        // 读取cmd的输出\n",
    "        using (StreamReader reader = process.StandardOutput)\n",
    "        {\n",
    "            string stdoutLine = reader.ReadToEnd();\n",
    "            Console.WriteLine(stdoutLine);\n",
    "        }\n",
    "    }\n",
    "}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "dotnet_interactive": {
     "language": "pwsh"
    },
    "polyglot_notebook": {
     "kernelName": "pwsh"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [],
   "source": [
    "#!set --value @csharp:global_netstat_filter --name queryFilter\n",
    "netstat -ano | findstr $queryFilter"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在这种情况下，我们可以看到使用了五个连接。其中的前四个在1秒后从池中删除，因此无法在下一个请求中重复使用。结果，每个请求都打开了一个新连接。现在，原始连接处于TIME_WAIT状态，并且操作系统无法将其重新用于新的出站连接。最终连接显示为ESTABLISHED，因为我在它过期之前就抓住了它。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 测试最大连接数"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    },
    "polyglot_notebook": {
     "kernelName": "csharp"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [],
   "source": [
    "/*\n",
    "\t功能：将MaxConnectionsPerServer限制为2。然后启动200个任务，每个任务都向同一端点发出HTTP请求。这些任务将同时运行。所有请求竞争所花费的时间将写入控制台。\n",
    "\t\t  随即调用用netstat命令查看连接：则根据定义的限制，我们可以看到两个已建立的连接。\n",
    "*/\n",
    "{\n",
    "\tConsole.WriteLine(\"开始请求网络...\");\n",
    "\tvar socketsHandler = new SocketsHttpHandler\n",
    "\t{\n",
    "\t\tPooledConnectionLifetime = TimeSpan.FromSeconds(60),\n",
    "\t\tPooledConnectionIdleTimeout = TimeSpan.FromMinutes(20),\n",
    "\t\tMaxConnectionsPerServer = 2\n",
    "\t};\n",
    "\n",
    "\tvar client = new HttpClient(socketsHandler)\n",
    "\t{\n",
    "\t\tBaseAddress = new Uri(global_api_config.BaseUrl)\n",
    "\t};\n",
    "\n",
    "\tvar sw = Stopwatch.StartNew();\n",
    "\n",
    "\tvar tasks = Enumerable.Range(0, 200).Select(i => client.GetAsync(global_default_page));\n",
    "\n",
    "\tawait Task.WhenAll(tasks);\n",
    "\n",
    "\tsw.Stop();\n",
    "\t\t\n",
    "\tConsole.WriteLine($\"共请求了200次，耗时 {sw.ElapsedMilliseconds} 毫秒\");\n",
    "\t\t\n",
    "\t//执行查看网络状态方法\n",
    "\tConsole.WriteLine(\"当前网络状态\");\n",
    "\tvar message = HttpClientStudy.Core.Utilities.AppUtility.RunCmd($\"netstat -ano | findstr {global_netstat_filter}\");\n",
    "\tConsole.WriteLine(message);\n",
    "}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "dotnet_interactive": {
     "language": "pwsh"
    },
    "polyglot_notebook": {
     "kernelName": "pwsh"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [],
   "source": [
    "# 重新查询当前网络状态\n",
    "#!set --value @csharp:global_netstat_filter --name queryFilter\n",
    "netstat -ano | findstr $queryFilter"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "如果我们调整此代码以允许MaxConnectionsPerServer = 10，则可以重新运行该应用程序。耗时将减少大约4倍。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    },
    "polyglot_notebook": {
     "kernelName": "csharp"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [],
   "source": [
    "{   //MaxConnectionsPerServer 设置为10：网络连接将增加到10个，耗时将减少到1/4\n",
    "\tConsole.WriteLine(\"开始请求网络...\");\n",
    "\tvar socketsHandler = new SocketsHttpHandler\n",
    "\t{\n",
    "\t\tPooledConnectionLifetime = TimeSpan.FromSeconds(60),\n",
    "\t\tPooledConnectionIdleTimeout = TimeSpan.FromMinutes(20),\n",
    "\t\tMaxConnectionsPerServer = 10\n",
    "\t};\n",
    "\n",
    "\tvar client = new HttpClient(socketsHandler)\n",
    "\t{\n",
    "\t\tBaseAddress = new Uri(global_api_config.BaseUrl)\n",
    "\t};\n",
    "\n",
    "\t//client.Display();\n",
    "\n",
    "\tvar sw = Stopwatch.StartNew();\n",
    "\n",
    "\tvar tasks = Enumerable.Range(0, 200).Select(i => client.GetAsync(global_default_page));\n",
    "\n",
    "\tawait Task.WhenAll(tasks);\n",
    "\n",
    "\tsw.Stop();\n",
    "\t\t\n",
    "\tConsole.WriteLine($\"共请求了200次，耗时 {sw.ElapsedMilliseconds} 毫秒\");\n",
    "\t\t\n",
    "\t//执行查看网络状态方法\n",
    "\tConsole.WriteLine(\"当前网络状态\");\n",
    "\tvar message = AppUtility.RunCmd($\"netstat -ano | findstr {global_netstat_filter}\");\n",
    "\tConsole.WriteLine(message);\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3、推荐使用方式"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "总则：\n",
    "\n",
    " 一、 应使用长期客户端（静态对象、单例等），并设置 PooledConnectionLifetime。这能解决DNS问题和套接字耗尽问题。\n",
    " \n",
    " 二、 使用 IHttpClientFactory 创建的短期客户端："
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "+ 在 .NET Core 和 .NET 5+ 中：\n",
    "  \n",
    "  + 根据预期的 DNS 更改，使用 static 或 singletonHttpClient 实例，并将 PooledConnectionLifetime 设置为所需间隔（例如 2 分钟）。 这可以解决端口耗尽和 DNS 更改两个问题，而且不会增加 IHttpClientFactory 的开销。 如果需要模拟处理程序，可以单独注册它。\n",
    "  \n",
    "  + 使用 IHttpClientFactory，可以针对不同的用例使用多个以不同方式配置的客户端。 但请注意，工厂创建的客户端生存期较短，一旦创建客户端，工厂就不再可以控制它。\n",
    "    工厂合并 HttpMessageHandler 实例，如果其生存期尚未过期，则当工厂创建新的 HttpClient 实例时，可以从池中重用处理程序。 这种重用避免了任何套接字耗尽问题。\n",
    "    如果需要 IHttpClientFactory 提供的可配置性，我们建议使用类型化客户端方法。\n",
    "\n",
    "+ 在 .NET Framework 中，使用 IHttpClientFactory 管理 HttpClient 实例。 如果不使用工厂，而是改为自行为每个请求创建新的客户端实例，则可能耗尽可用的端口。 \n",
    "\n",
    "`提示`: 如果应用需要 Cookie，请考虑禁用自动 Cookie 处理或避免使用 IHttpClientFactory。 共用 HttpMessageHandler 实例会导致共享 CookieContainer 对象。 意外的 CookieContainer 对象共享通常会导致错误的代码。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    },
    "polyglot_notebook": {
     "kernelName": "csharp"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [],
   "source": [
    "{ //不推荐的示例\n",
    "   int requestCount =0;\n",
    "\n",
    "    //这会建立10个 HttpClient \n",
    "    //尽管使用了Using，不过Using只保证应用进程释放实例；但是http请求是跨操作系统、跨网络的操作，调用Using的进程管不了操作系统，更管不了网络。\n",
    "    //如果把循环次数加大到 65535 就会一定导致夏套接字耗尽(2000以很可能就会出现)。\n",
    "   Parallel.For(0,10,async (a,b)=>\n",
    "   {\n",
    "        using (var client = new HttpClient())\n",
    "        {\n",
    "            _ = await client.GetAsync (global_api_config.BaseUrl + global_default_page);\n",
    "        }   \n",
    "        Interlocked.Add(ref requestCount, 1);\n",
    "    });\n",
    "}\n",
    "\n",
    "{ //使用长期客户端\n",
    "   using (var client = new HttpClient())\n",
    "   {\n",
    "        client.BaseAddress = new Uri(global_api_config.BaseUrl);\n",
    "        \n",
    "        for(int i=0; i<10; i++)\n",
    "        {\n",
    "            //n次调用，均使用同一个 HttpClient 实例\n",
    "            _ = await client.GetAsync(global_default_page);\n",
    "        }\n",
    "   }// 所有调用完成，才释放 HttpClient 实例\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4、静态客户端的复原能力"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    },
    "polyglot_notebook": {
     "kernelName": "csharp"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [],
   "source": [
    "#r \"nuget:Polly\"\n",
    "#r \"nuget:Microsoft.Extensions.Http.Resilience\"\n",
    "using System;\n",
    "using System.Net.Http;\n",
    "using Microsoft.Extensions.Http;\n",
    "using Microsoft.Extensions.Http.Resilience;\n",
    "using Polly;\n",
    "\n",
    "{\n",
    "    var retryPipeline = new ResiliencePipelineBuilder<HttpResponseMessage>()\n",
    "    .AddRetry(new HttpRetryStrategyOptions\n",
    "    {\n",
    "        BackoffType = DelayBackoffType.Exponential,\n",
    "        MaxRetryAttempts = 3\n",
    "    })\n",
    "    .Build();\n",
    "\n",
    "    var socketHandler = new SocketsHttpHandler\n",
    "    {\n",
    "        PooledConnectionLifetime = TimeSpan.FromMinutes(15)\n",
    "    };\n",
    "\n",
    "    #pragma warning disable EXTEXP0001\n",
    "    var resilienceHandler = new ResilienceHandler(retryPipeline)\n",
    "    {\n",
    "        InnerHandler = socketHandler,\n",
    "    };\n",
    "    #pragma warning restore EXTEXP0001\n",
    "\n",
    "    var httpClient = new HttpClient(resilienceHandler);\n",
    "    httpClient.BaseAddress = new Uri(global_api_config.BaseUrl);\n",
    "\n",
    "    var response = await httpClient.GetAsync(global_default_page);\n",
    "    var htmlText = await response.Content.ReadAsStringAsync();\n",
    "    Console.WriteLine($\"共有{htmlText.Length}个字符\");\n",
    "}\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".NET (C#)",
   "language": "C#",
   "name": ".net-csharp"
  },
  "language_info": {
   "name": "python"
  },
  "polyglot_notebook": {
   "kernelInfo": {
    "defaultKernelName": "csharp",
    "items": [
     {
      "aliases": [],
      "name": "csharp"
     }
    ]
   }
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
